D:\a\cssh-rs\cssh-rs\xtask\src\record_demo.rs
Line | Count | Source |
1 | | //! Record the cssh-rs demo GIF via the `demo/` package. |
2 | | //! |
3 | | //! The Rust side is a thin shell: it builds the binary and installs the |
4 | | //! packages, then the `demo/suites/record_demo.robot` suite drives real |
5 | | //! console windows and records the desktop. Windows only. |
6 | | |
7 | | use std::path::{Path, PathBuf}; |
8 | | |
9 | | use anyhow::{bail, Context, Result}; |
10 | | |
11 | | /// All side-effecting operations required by this module. |
12 | | /// |
13 | | /// Implement with mocks in tests to achieve zero process and filesystem |
14 | | /// side-effects. |
15 | | pub trait DemoSystem { |
16 | | /// Return `true` when running on Windows, the demo's only supported host. |
17 | | fn is_windows(&self) -> bool; |
18 | | |
19 | | /// Return the absolute path to the workspace root (parent of `xtask/`). |
20 | | /// |
21 | | /// # Errors |
22 | | /// Returns an error if the workspace root cannot be resolved. |
23 | | fn workspace_root(&self) -> Result<PathBuf>; |
24 | | |
25 | | /// Build the debug cssh-rs binary and return its path. |
26 | | /// |
27 | | /// # Errors |
28 | | /// Returns an error if the build fails. |
29 | | fn build_binary(&self) -> Result<PathBuf>; |
30 | | |
31 | | /// Install a Python package directory in editable mode. |
32 | | /// |
33 | | /// # Arguments |
34 | | /// * `package_dir` - Directory holding the package's `pyproject.toml`. |
35 | | /// |
36 | | /// # Errors |
37 | | /// Returns an error if the install fails. |
38 | | fn pip_install(&self, package_dir: &Path) -> Result<()>; |
39 | | |
40 | | /// Run the demo recorder robot suite to produce the GIF. |
41 | | /// |
42 | | /// # Arguments |
43 | | /// * `binary` - Path to the cssh-rs executable to drive. |
44 | | /// * `output_dir` - Directory for the intermediate MP4. |
45 | | /// * `gif` - Destination path for the exported GIF. |
46 | | /// |
47 | | /// # Errors |
48 | | /// Returns an error if the recorder exits with a non-zero status. |
49 | | fn run_demo(&self, binary: &Path, output_dir: &Path, gif: &Path) -> Result<()>; |
50 | | |
51 | | /// Return `true` when `path` exists. |
52 | | /// |
53 | | /// # Arguments |
54 | | /// * `path` - Filesystem path to probe. |
55 | | fn path_exists(&self, path: &Path) -> bool; |
56 | | } |
57 | | |
58 | | /// Production implementation of [`DemoSystem`]. |
59 | | pub struct RealSystem; |
60 | | |
61 | | #[cfg_attr(coverage_nightly, coverage(off))] |
62 | | impl DemoSystem for RealSystem { |
63 | | fn is_windows(&self) -> bool { |
64 | | cfg!(windows) |
65 | | } |
66 | | |
67 | | fn workspace_root(&self) -> Result<PathBuf> { |
68 | | // CARGO_MANIFEST_DIR points at xtask/, whose parent is the workspace root. |
69 | | Ok(Path::new(env!("CARGO_MANIFEST_DIR")) |
70 | | .parent() |
71 | | .context("failed to resolve workspace root from CARGO_MANIFEST_DIR")? |
72 | | .to_path_buf()) |
73 | | } |
74 | | |
75 | | fn build_binary(&self) -> Result<PathBuf> { |
76 | | let root = self.workspace_root()?; |
77 | | let status = std::process::Command::new("cargo") |
78 | | .args(["build", "-p", "cssh-rs"]) |
79 | | .status() |
80 | | .context("failed to spawn `cargo build -p cssh-rs`")?; |
81 | | if !status.success() { |
82 | | bail!("`cargo build -p cssh-rs` failed with status {status}"); |
83 | | } |
84 | | Ok(root.join("target").join("debug").join("cssh-rs.exe")) |
85 | | } |
86 | | |
87 | | fn pip_install(&self, package_dir: &Path) -> Result<()> { |
88 | | let status = std::process::Command::new("python") |
89 | | .args(["-m", "pip", "install", "-e"]) |
90 | | .arg(package_dir) |
91 | | .status() |
92 | | .context("failed to spawn `python -m pip install`; is Python on PATH?")?; |
93 | | if !status.success() { |
94 | | bail!( |
95 | | "`pip install -e {}` failed with status {status}", |
96 | | package_dir.display() |
97 | | ); |
98 | | } |
99 | | Ok(()) |
100 | | } |
101 | | |
102 | | fn run_demo(&self, binary: &Path, output_dir: &Path, gif: &Path) -> Result<()> { |
103 | | let suite = self |
104 | | .workspace_root()? |
105 | | .join("demo") |
106 | | .join("suites") |
107 | | .join("record_demo.robot"); |
108 | | // `python -m robot` uses the interpreter that installed the packages; a |
109 | | // bare `robot` needs the Scripts dir on PATH, which is not guaranteed. |
110 | | // `--variable NAME:VALUE` splits on the first colon, so `C:\...` survives. |
111 | | let status = std::process::Command::new("python") |
112 | | .args(["-m", "robot"]) |
113 | | .arg("--variable") |
114 | | .arg(format!("BINARY:{}", binary.display())) |
115 | | .arg("--variable") |
116 | | .arg(format!("OUTPUT_DIR:{}", output_dir.display())) |
117 | | .arg("--variable") |
118 | | .arg(format!("GIF:{}", gif.display())) |
119 | | .arg("--outputdir") |
120 | | .arg(output_dir) |
121 | | .arg(&suite) |
122 | | .status() |
123 | | .context("failed to spawn `python -m robot`; is Python on PATH and the demo package installed?")?; |
124 | | if !status.success() { |
125 | | bail!("demo recorder (robot) exited with status {status}"); |
126 | | } |
127 | | Ok(()) |
128 | | } |
129 | | |
130 | | fn path_exists(&self, path: &Path) -> bool { |
131 | | path.exists() |
132 | | } |
133 | | } |
134 | | |
135 | | /// Record the demo GIF into `target/demo/cssh-rs.gif`. |
136 | | /// |
137 | | /// The GIF lands under `target/` so Cargo's `.gitignore` covers it; |
138 | | /// publishing it is a separate step. |
139 | | /// |
140 | | /// # Arguments |
141 | | /// * `system` - Injected I/O provider. |
142 | | /// |
143 | | /// # Errors |
144 | | /// Returns an error on a non-Windows host, or if any build, install or record |
145 | | /// step fails, or if no GIF is produced. |
146 | 3 | pub fn record_demo<S: DemoSystem>(system: &S) -> Result<()> { |
147 | 3 | if !system.is_windows() { |
148 | 1 | bail!("`record-demo` runs on Windows only: it drives real console windows and records the desktop"); |
149 | 2 | } |
150 | 2 | let root = system.workspace_root()?0 ; |
151 | | |
152 | 2 | log::info!("Building cssh-rs (debug) for the demo"); |
153 | 2 | let binary = system.build_binary()?0 ; |
154 | | |
155 | 4 | for package in ["automation", "demo"]2 { |
156 | 4 | let dir = root.join(package); |
157 | 4 | log::info!("Installing {} (editable)", dir.display()); |
158 | 4 | system.pip_install(&dir)?0 ; |
159 | | } |
160 | | |
161 | 2 | let output_dir = root.join("target").join("demo"); |
162 | 2 | let gif = output_dir.join("cssh-rs.gif"); |
163 | 2 | log::info!("Recording demo -> {}", gif.display()); |
164 | 2 | system.run_demo(&binary, &output_dir, &gif)?0 ; |
165 | | |
166 | 2 | if !system.path_exists(&gif) { |
167 | 1 | bail!( |
168 | | "demo recorder finished but no GIF was written at {}", |
169 | 1 | gif.display() |
170 | | ); |
171 | 1 | } |
172 | 1 | log::info!("Wrote demo GIF -> {}", gif.display()); |
173 | 1 | Ok(()) |
174 | 3 | } |
175 | | |
176 | | #[cfg(test)] |
177 | | #[path = "tests/test_record_demo.rs"] |
178 | | mod tests; |